package sk.naive.talker.server;

import sk.naive.talker.*;
import sk.naive.talker.AdapterProxy;
import sk.naive.talker.plugin.*;
import sk.naive.talker.message.*;
import sk.naive.talker.util.*;
import sk.naive.talker.persistence.*;
import sk.naive.talker.callback.*;
import sk.naive.talker.props.*;
import sk.naive.talker.command.*;
import sk.naive.talker.adapter.*;

import java.rmi.*;
import java.util.*;
import java.util.Set;
import java.util.logging.*;
import java.io.IOException;

/**
 * Talker server implementation (Naive Talker).
 * <p>
 * Talker implements kernel interface. Actually it implements also remote interface,
 * but this is not declared - all methods are called from RMI server delegate.
 * <p>
 * <i>This separation of roles RMI server/talker is implemented because
 * RMI server needs to extend UnicastRemoteObject and Talker needs to
 * extend AbstractPersistentObject.</i>
 *
 * @see TalkerServer
 * @author <a href="mailto:virgo@naive.deepblue.sk">Richard "Virgo" Richter</a>
 * @version $Revision: 1.134 $ $Date: 2007-05-07 10:34:20 $
 */
public final class Talker implements sk.naive.talker.Talker {
	private Logger logger;
	private CommandDispatcher commandDispatcher;
	private List<TalkerPlugin> plugins;
	private TimerService timer;

	private Map<AdapterUser,UserProxy> remoteUserUserProxy;

	/** List of registered adapters. */
	private List<AdapterProxy> adapters;
	private Map<Adapter, AdapterProxy> remoteAdapterAdapterProxy;

	/** Message factory for talker. */
	private MessageFactory messageFactory;

	private LocationRegistry locationRegistry;
	private ThingRegistry thingRegistry;

	private UserFactory userFactory;
	private UserRegistry userRegistry;

	private BoardRegistry boardRegistry;

	private PersistentPropertyStoreImpl store;

	/** Inicializacia Talkera. */
	public Talker(Integer id, PropertyConfiguration pc, UserFactory uf, BoardFactory bf)
		throws PersistenceException, RemoteException
	{
		userFactory = uf;

		logger = Logger.getLogger("nt.kernel");
		adapters = new LinkedList<AdapterProxy>();
		remoteUserUserProxy = new HashMap<AdapterUser,UserProxy>();
		remoteAdapterAdapterProxy = new HashMap<Adapter, AdapterProxy>();

		DbLayer db = new DbLayer();
		store = new PersistentPropertyStoreImpl(id, pc);
		db.loadProperties(this);

		ReloadableResources rr = new ReloadableResources();
		getProperties().put(ReloadableResources.TPROP_RELOAD_HELPER, rr);

		getProperties().put(Consts.TPROP_CREATED, System.currentTimeMillis());

		messageFactory = new DefaultMessageFactory("sk/naive/talker/server/messages");
		rr.add((Reloadable) messageFactory);
		commandDispatcher = new CommandDispatcher(this);

		userRegistry = new UserRegistry(db.userIdLoginMap(), userFactory, logger);
		boardRegistry = bf.createBoardFinder(db.boardNames(), logger);
		locationRegistry = new LocationRegistry(
			"sk/naive/talker/server/LocationPropertyResources",
			db.locationNames(),
			this
		);
		// TODO: veci
//		thingRegistry = new ThingRegistry(
//			"sk/naive/talker/server/ThingPropertyResources",
//			this
//		);

		timer = new Timer("Naive timer service");

		PluginHelper pluginHelper = new PluginHelper();
		plugins = new ArrayList<TalkerPlugin>();
		List<String> loadedPluginNames = pluginNames();
		getProperties().put(Consts.TPROP_PLUGIN_NAMES, new LinkedList<String>());
		for (String pluginName : loadedPluginNames) {
			try {
				TalkerPlugin plugin = pluginHelper.createPlugin(pluginName);
				if (plugin != null) {
					addPlugin(plugin);
				} else {
					logger.warning("Plugin '" + pluginName + "' was not found!");
				}
			} catch (Exception e) {
				logger.log(Level.WARNING, "Plugin '" + pluginName + "' was NOT succesfully loaded!", e);
			}
		}
	}

	private List<String> pluginNames() {
		return (List<String>) get(Consts.TPROP_PLUGIN_NAMES);
	}

	public void addPlugin(TalkerPlugin plugin) throws PluginException {
		if (pluginNames().contains(plugin.name())) {
			throw new PluginException("There is already loaded plugin with name '" + plugin.name() + "'!");
		}
		plugin.init(this);
		pluginNames().add(plugin.name());
		commandDispatcher.registerPlugin(plugin);
		plugins.add(plugin);
		logger.info("Plugin '" + plugin.name() + "' loaded into the talker.");
	}

	public void removePlugin(TalkerPlugin plugin) {
		plugins.remove(plugin);
		commandDispatcher.unregisterPlugin(plugin);
		plugin.destroy();
		pluginNames().remove(plugin.name());
		logger.info("Plugin '" + plugin.name() + "' unloaded from the talker.");
	}

	public String processSystemCommand(String command) {
		if (command == null || command.length() < 0) {
			return "Command is null";
		}
		command = command.toLowerCase();
		if ("reset-timer".startsWith(command)) {
			timer.cancel();
			timer = new Timer("Naive timer service");
			return "OK: reset-timer";
		}
		return "Unknown command";
	}

	public void registerAdapter(Adapter remoteAdapter, RemotePropertyConfiguration rpc)
		throws RemoteException
	{
		try {
			Set<String> remoteProperties = new HashSet<String>();
			remoteProperties.addAll(rpc.getUserSettable());
			remoteProperties.addAll(rpc.getUserPersistent());
			userFactory.addAdapterConfiguration(rpc.getUserSettable(), rpc.getUserPersistent(), rpc.getUserProperties());

			AdapterProxy adapter = new sk.naive.talker.server.AdapterProxy(remoteAdapter, rpc.getAdapterSettable(), rpc.getAdapterPersistent(), Collections.unmodifiableSet(remoteProperties));
			adapters.add(adapter);
			remoteAdapterAdapterProxy.put(remoteAdapter, adapter);

			logger.info("Adapter " + adapter.getName() + " was registered at Talker kernel.");
		} catch (Exception e) {
			throw new RemoteException(e.getMessage(), e);
		}
	}

	public void deregisterAdapter(Adapter adapter) throws RemoteException {
		AdapterProxy ap = remoteAdapterAdapterProxy.get(adapter);
		remoteAdapterAdapterProxy.remove(adapter);
		adapters.remove(ap);
		logger.info("Adapter " + ap.getName() + " was deregistered from Talker kernel.");
	}

	public String processSystemMessage(String message) throws RemoteException {
		logger.finest("system: " + message);
		String sa[] = Utils.split(message, "\n", 0);
		if (sa.length == 0) {
			return null;
		}
		String cmd = sa[0];
		if (cmd.equals("userInfo")) {
			try {
				return String.valueOf(new DbLayer().userId(sa[1]));
			} catch (PersistenceException e) {
				throw new RemoteException("persistence exception", e);
			}
		}
		if (cmd.equals("who")) {
			List<String> logins = new ArrayList<String>(userRegistry.onlineUsers().size());
			for (User user : userRegistry.onlineUsers()) {
				logins.add(user.getLogin());
			}
			return Utils.formatCollection(logins, "\n");
		}
		return null;
	}

	public void processUserCallback(AdapterUser u, CallbackResult result)
		throws RemoteException
	{
		UserProxy user = remoteUserUserProxy.get(u);
		try {
			CommandCallback cc = user.useCommandCallback(result.callbackId());
			if (cc != null) {
				UserTimeStatisticsCalculator utsc = new UserTimeStatisticsCalculator(user);
				utsc.resetAfkIdle(this, result.value());
				utsc.updateLastAction();
				if (result.action() == CallbackResult.CALLBACK_OK) {
					cc.process(result.value());
					logger.finest("Command callback " + result.callbackId() + " processed");
				} else {
					cc.cancel();
					logger.finest("Command callback " + result.callbackId() + " cancelled");
				}
			}
		} catch (CallbackException e) {
			logger.log(Level.WARNING, "Command callback process exception", e);
			user.send(messageFactory.getString("error.commandException", user.getProperties()));
		} catch (PropertyStoreException e) {
			logger.log(Level.WARNING, "Property store exception", e);
			user.send(messageFactory.getString("error.commandException", user.getProperties()));
		} catch (Exception e) {
			Utils.unexpectedExceptionWarning(e);
			user.send(messageFactory.getString("error.exception", user.getProperties()));
		} catch (Error e) {
			logger.log(Level.SEVERE, "Fatal error - immediate shutdown", e);
			user.send(messageFactory.getString("error.exception", user.getProperties()));
//			shutdown();
		}
	}

	/**
	 * @param remoteUser Pouzivatel.
	 * @param message Samotny text.
	 */
	public void processUserMessage(AdapterUser remoteUser, String message)
		throws RemoteException
	{
		UserProxy user = remoteUserUserProxy.get(remoteUser);
		if (user == null) {
			logger.warning("Processing user message for not logged in user. (message=\"" + message + "\", user=" + user + ")");
			remoteUser.getAdapter().disconnect(remoteUser);
			return;
		}
		try {
			UserTimeStatisticsCalculator utsc = new UserTimeStatisticsCalculator(user);
			utsc.resetAfkIdle(this, message);
			if (message != null && message.length() > 0) {
				utsc.updateLastAction();
			}
			logger.finest(user.getLogin() + ": " + message);

			new UserMessageProcessor(this, user, message).process();
		} catch (CommandException e) {
			Level level = Level.WARNING;
			if (e instanceof PermissionException) {
				level = Level.FINEST;
			}
			logger.log(level, "Command process exception", e);
			user.send(messageFactory.getString("error.commandException", user.getProperties()));
		} catch (Exception e) {
			Utils.unexpectedExceptionWarning(e);
			user.send(messageFactory.getString("error.exception", user.getProperties()));
		} catch (Error e) {
			logger.log(Level.SEVERE, "Fatal error - immediate shutdown", e);
			user.send(messageFactory.getString("error.exception", user.getProperties()));
//			shutdown();
		}
	}

	/**
	 * Method tries to authentize user and if it succeed user is logged in talker.
	 * <p>
	 * If user login succeed the talker set these remote user properties:
	 * <ul>
	 * <li><b>userId</b> why not? Something for user identification.
	 * <li><b>lang</b> for adapter side messages (editor, pager)
	 * </ul>
	 *
	 * @param remoteUser native adapter user which is trying to log in
	 * @throws RemoteException thrown by RMI
	 * @throws AuthenticationException thrown when authentization failed
	 */
	public void userIn(AdapterUser remoteUser)
		throws RemoteException,
		AuthenticationException,
		PersistenceException
	{
		UserLoginHelper ulh = new UserLoginHelper(this, remoteUser); //TODO ak sa da .process();
		UserProxy up = userFactory.createOnlineUser(
			ulh.authenticate(),
			remoteUser,
			remoteAdapterAdapterProxy.get(remoteUser.getAdapter())
		);
		boolean relogin = false;
		try {
			relogin = reloginCheck(up);
			long current = System.currentTimeMillis();
			up.set(User.UPROP_LAST_ACTION_TIME, current);
			if (!relogin) {
				new DbLayer().loadProperties(up);
				up.getProperties().put(User.UPROP_LOGIN_TIME, current);
				up.set(User.UPROP_LAST_TLT_SAVE, current);
			}
			remoteUser.set(User.UPROP_ID, up.getId());
			remoteUserUserProxy.put(remoteUser, up);
			userRegistry.login(up);
			up.getProperties().put(User.UPROP_PLAIN_PASSWORD, remoteUser.get(User.UPROP_PASSWORD));
			up.set(Quit.UPROP_QUIT_MESSAGE, null);
		} catch (RemoteException e) {
			logger.log(Level.WARNING, "Exception occured while loading properties...", e);
		} catch (PropertyStoreException e) {
			logger.log(Level.WARNING, "Exception occured while loading properties...", e);
		}
		try {
			ulh.welcomeUser(up, relogin);
		} catch (Exception e) {
			Utils.unexpectedExceptionWarning(e);
		}
	}

	// TODO potencialne presunut aj tieto dve metody neskor
	private boolean reloginCheck(UserProxy newUser) throws RemoteException, PropertyStoreException {
		for (AdapterUser ruKey : remoteUserUserProxy.keySet()) {
			User oldUser = remoteUserUserProxy.get(ruKey);
			if (oldUser.getId().equals(newUser.getId())) {
				remoteUserUserProxy.remove(ruKey);
				performRelogin(oldUser, newUser);
				return true;
			}
		}
		return false;
	}

	private void performRelogin(User oldUser, UserProxy newUser) throws RemoteException, PropertyStoreException {
		UserTimeStatisticsCalculator utsc = new UserTimeStatisticsCalculator(oldUser);
		utsc.resetAfkIdle(this, "relogin");
		oldUser.send(messageFactory.getString("server.reconnect", newUser.getProperties()));
		for (String key : oldUser.getProperties().keySet()) {
			newUser.setNonpersistent(key, oldUser.getProperties().get(key));
		}
		Location oldUserLocation = locationRegistry.findLocationForUser(oldUser);
		if (oldUserLocation != null) {
			oldUserLocation.userIn(newUser);
		}
		userOutPrivate(oldUser);
	}

	/**
	 * Odhlasenie pouzivatela.
	 */
	public void userOut(AdapterUser u)
		throws RemoteException
	{
		User user = remoteUserUserProxy.get(u);
		userOut(user);
	}

	public void userOut(User user)
		throws RemoteException
	{
		for (TalkerPlugin plugin : plugins) {
			try {
				plugin.userOut(user);
			} catch (Throwable t) {
				Utils.unexpectedExceptionWarning(t);
			}
		}
		String adapterReason = (String) user.getRemoteUser().get(AdapterUser.UPROP_DISCONNECT_REASON);
		try {
			UserTimeStatisticsCalculator utsc = new UserTimeStatisticsCalculator(user);
			utsc.updateLogoutInformation();
			utsc.resetAfkIdle(this, null);
			if (locationRegistry.findLocationForUser(user) != null) {
				user.set(User.UPROP_LAST_LOCATION, locationRegistry.findLocationForUser(user).getName());
			}
			if (adapterReason != null) {
				user.set(Quit.UPROP_QUIT_MESSAGE, adapterReason);
				adapterReason = "(" + adapterReason + ")";
			} else {
				adapterReason = "";
			}
		} catch (PropertyStoreException e) {
			Utils.unexpectedExceptionWarning(e);
		}
		userOutPrivate(user);
		saveLastInfo(user);
		logger.fine("Out: " + user.getLogin() + " " + adapterReason);
		String msgKey = "server.logout";
		String quitMessage = (String) user.getProperties().get(Quit.UPROP_QUIT_MESSAGE);
		if (quitMessage != null) {
			msgKey = "server.logoutWithMessage";
		}
		sendToUsers(messageFactory.createMessage(msgKey, user.getProperties()), userFinder().onlineUsers(), new UserIgnoreExcluder(null, Category.SYSTEM));
	}

	private void saveLastInfo(User user) {
		Buffer lasts = (Buffer) getProperties().get(Consts.TPROP_LASTS);
		if (lasts == null) {
			lasts = new Buffer(32);
			getProperties().put(Consts.TPROP_LASTS, lasts);
		}
		UserContextHelper userContextHelper = new UserContextHelper(user);
		userContextHelper.set(DefaultMessageFactory.CTXKEY_VAL + "session", Utils.createLong(user.getProperties().get(User.UPROP_LAST_SESSION_TIME), Consts.MINUTE_MILLIS));
		userContextHelper.set(DefaultMessageFactory.CTXKEY_VAL + "time", new Date((Long) user.getProperties().get(User.UPROP_LAST_LOGIN)));
		lasts.add(messageFactory.createMessage("last.item", user.getProperties()));
		userContextHelper.cleanup();
	}

	private void userOutPrivate(User user)
		throws RemoteException
	{
		user.getAdapter().disconnect(user);
		userRegistry.logout(user);
		remoteUserUserProxy.remove(user.getRemoteUser());
		Location loc = locationRegistry.findLocationForUser(user);
		if (loc != null) {
			loc.userOut(user);
		}
	}

	public void shutdown()
		throws RemoteException
	{
		for (Iterator<User> i = userRegistry.onlineUsers().iterator(); i.hasNext(); ) {
			User user = i.next();
			user.send(messageFactory.getString("error.fatal", user.getProperties()));
			i.remove();
			userOut(user);
		}
		for (Iterator<AdapterProxy> i = adapters.iterator(); i.hasNext(); ) {
			sk.naive.talker.AdapterProxy a = i.next();
			i.remove();
			try {
				a.shutdown();
			} catch (IOException e) {
				Utils.unexpectedExceptionWarning(e);
			}
		}
		try {
			Main.shutdownServices();
			// There is an expectation that after this whole process
			// will finish and registry will be shut down along.
			logger.severe("Talker shutdown... OK...");
		} catch (Exception e) {
			Utils.unexpectedExceptionWarning(e);
		}
	}

	public void sendToUsers(Message m, Collection<User> users, UserExcluder e)
		throws RemoteException
	{
		for (User user : users) {
			if (e != null && e.isExcluded(user)) {
				continue;
			}
			user.send(m);
		}
	}

	public List<AdapterProxy> getAdapters() {
		return adapters;
	}

	public MessageFactory messageFactory() {
		return messageFactory;
	}

	public LocationFinder locationFinder() {
		return locationRegistry;
	}

	public UserFinder userFinder() {
		return userRegistry;
	}

	public BoardRegistry boardFinder() {
		return boardRegistry;
	}

	public List<TalkerPlugin> getPlugins() {
		return plugins;
	}

	public TimerService timerService() {
		return timer;
	}

	public Logger getLogger() {
		return logger;
	}

	public CommandDispatcher getCommandDispatcher() {
		return commandDispatcher;
	}

	public Set<String> getSettable() {
		return store.getSettable();
	}

	public Set<String> getPersistent() {
		return store.getPersistent();
	}

	public Property getProperty(String key) {
		return store.getProperty(key);
	}

	public Integer getId() {
		return store.getId();
	}

	public void setStoredValue(String key, String value) throws PropertyStoreException {
		store.setStoredValue(key, value);
	}

	public Object get(String key) {
		return store.get(key);
	}

	public void set(String property, Object value) throws PropertyStoreException {
		store.set(property, value);
	}

	public Map<String,Object> getProperties() {
		return store.getProperties();
	}

	public ThingFinder thingFinder() {
		return thingRegistry;
	}
}
